1 /** 2 Copyright: Copyright (c) 2016-2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Utility functions for Clang Compilation Databases. 11 */ 12 module code_checker.compile_db; 13 14 import std.json : JSONValue; 15 import std.typecons : Nullable; 16 import logger = std.experimental.logger; 17 import std.exception : collectException; 18 19 import code_checker.types : AbsolutePath; 20 21 public import code_checker.compile_db.user_filerange; 22 23 version (unittest) { 24 import std.path : buildPath; 25 import unit_threaded : Name, shouldEqual; 26 } 27 28 @safe: 29 30 /** Hold an entry from the compilation database. 31 * 32 * The following information is from the official specification. 33 * $(LINK2 http://clang.llvm.org/docs/JSONCompilationDatabase.html, Standard) 34 * 35 * directory: The working directory of the compilation. All paths specified in 36 * the command or file fields must be either absolute or relative to this 37 * directory. 38 * 39 * file: The main translation unit source processed by this compilation step. 40 * This is used by tools as the key into the compilation database. There can be 41 * multiple command objects for the same file, for example if the same source 42 * file is compiled with different configurations. 43 * 44 * command: The compile command executed. After JSON unescaping, this must be a 45 * valid command to rerun the exact compilation step for the translation unit 46 * in the environment the build system uses. Parameters use shell quoting and 47 * shell escaping of quotes, with ‘"‘ and ‘\‘ being the only special 48 * characters. Shell expansion is not supported. 49 * 50 * argumets: The compile command executed as list of strings. Either arguments 51 * or command is required. 52 * 53 * output: The name of the output created by this compilation step. This field 54 * is optional. It can be used to distinguish different processing modes of the 55 * same input file. 56 * 57 * Dextool additions. 58 * The standard do not specify how to treat "directory" when it is a relative 59 * path. The logic chosen in dextool is to treat it as relative to the path 60 * the compilation database file is read from. 61 */ 62 @safe struct CompileCommand { 63 import code_checker.types : DirName; 64 65 static import code_checker.types; 66 67 /// The raw filename from the tuples "file" value. 68 alias FileName = code_checker.types.FileName; 69 70 /// The combination of the tuples "file" and "directory" value. 71 static struct AbsoluteFileName { 72 code_checker.types.AbsoluteFileName payload; 73 alias payload this; 74 75 this(AbsoluteDirectory work_dir, string raw_path) { 76 payload = AbsolutePath(FileName(raw_path), DirName(work_dir)); 77 } 78 } 79 80 /// The tuples "directory" value converted to the absolute path. 81 static struct AbsoluteDirectory { 82 code_checker.types.AbsoluteDirectory payload; 83 alias payload this; 84 85 this(AbsoluteCompileDbDirectory db_path, string raw_path) { 86 payload = AbsolutePath(FileName(raw_path), DirName(db_path)); 87 } 88 } 89 90 /// The raw command from the tuples "command" value. 91 static struct Command { 92 string[] payload; 93 alias payload this; 94 bool hasValue() @safe pure nothrow const @nogc { 95 return payload.length != 0; 96 } 97 } 98 99 /// The raw arguments from the tuples "arguments" value. 100 static struct Arguments { 101 string[] payload; 102 alias payload this; 103 bool hasValue() @safe pure nothrow const @nogc { 104 return payload.length != 0; 105 } 106 } 107 108 /// The path to the output from running the command 109 static struct Output { 110 string payload; 111 alias payload this; 112 bool hasValue() @safe pure nothrow const @nogc { 113 return payload.length != 0; 114 } 115 } 116 117 /// 118 FileName file; 119 /// 120 AbsoluteFileName absoluteFile; 121 /// 122 AbsoluteDirectory directory; 123 /// 124 Command command; 125 /// 126 Arguments arguments; 127 /// 128 Output output; 129 /// 130 AbsoluteFileName absoluteOutput; 131 } 132 133 /// The path to the compilation database. 134 struct CompileDbFile { 135 string payload; 136 alias payload this; 137 } 138 139 /// The absolute path to the directory the compilation database reside at. 140 struct AbsoluteCompileDbDirectory { 141 string payload; 142 alias payload this; 143 144 invariant { 145 import std.path : isAbsolute; 146 147 assert(payload.isAbsolute); 148 } 149 150 this(string file_path) { 151 import std.path : buildNormalizedPath, dirName, absolutePath; 152 153 payload = buildNormalizedPath(file_path).absolutePath.dirName; 154 } 155 156 this(CompileDbFile db) { 157 this(cast(string) db); 158 } 159 160 unittest { 161 import std.path; 162 163 auto dir = AbsoluteCompileDbDirectory("."); 164 assert(dir.isAbsolute); 165 } 166 } 167 168 /// A complete compilation database. 169 struct CompileCommandDB { 170 CompileCommand[] payload; 171 alias payload this; 172 } 173 174 // The result of searching for a file in a compilation DB. 175 // The file may be occur more than one time therefor an array. 176 struct CompileCommandSearch { 177 CompileCommand[] payload; 178 alias payload this; 179 } 180 181 /** 182 * Trusted: opIndex for JSONValue is @safe in DMD-2.077.0 183 * remove the trusted attribute when the minimal requirement is upgraded. 184 */ 185 private Nullable!CompileCommand toCompileCommand(JSONValue v, AbsoluteCompileDbDirectory db_dir) nothrow @trusted { 186 import std.algorithm : map, filter, joiner, splitter; 187 import std.array : array; 188 import std.exception : assumeUnique; 189 import std.json : JSON_TYPE; 190 import std.range : only; 191 import std.utf : byUTF; 192 193 string[] command; 194 try { 195 command = v["command"].str.splitter.filter!(a => a.length != 0).array; 196 } catch (Exception ex) { 197 } 198 199 string[] arguments; 200 try { 201 enum j_arg = "arguments"; 202 const auto j_type = v[j_arg].type; 203 if (j_type == JSON_TYPE.STRING) 204 arguments = v[j_arg].str.splitter.filter!(a => a.length != 0).array; 205 else if (j_type == JSON_TYPE.ARRAY) { 206 import std.range; 207 208 // TODO unnecessary to join it 209 arguments = v[j_arg].arrayNoRef.filter!(a => a.type == JSON_TYPE.STRING) 210 .map!(a => a.str).filter!(a => a.length != 0).array; 211 } 212 } catch (Exception ex) { 213 } 214 215 if (command.length == 0 && arguments.length == 0) { 216 logger.error("Unable to parse json tuple, both command and arguments are empty") 217 .collectException; 218 return typeof(return)(); 219 } 220 221 string output; 222 try { 223 output = v["output"].str; 224 } catch (Exception ex) { 225 } 226 227 try { 228 const directory = v["directory"]; 229 const file = v["file"]; 230 231 foreach (a; only(directory, file).map!(a => !a.isNull 232 && a.type == JSON_TYPE.STRING).filter!(a => !a)) { 233 // sanity check. 234 // if any element is false then break early. 235 return typeof(return)(); 236 } 237 238 return toCompileCommand(directory.str, file.str, command, db_dir, arguments, output); 239 } catch (Exception ex) { 240 logger.error("Unable to parse json: " ~ ex.msg).collectException; 241 } 242 243 return typeof(return)(); 244 } 245 246 /** Transform a json entry to a CompileCommand. 247 * 248 * This function is under no circumstances meant to be exposed outside this module. 249 * The API is badly designed for common use because it relies on the position 250 * order of the strings for their meaning. 251 */ 252 private Nullable!CompileCommand toCompileCommand(string directory, string file, 253 string[] command, AbsoluteCompileDbDirectory db_dir, string[] arguments, string output) nothrow { 254 // expects that v is a tuple of 3 json values with the keys directory, 255 // command, file 256 257 Nullable!CompileCommand rval; 258 259 try { 260 auto abs_workdir = CompileCommand.AbsoluteDirectory(db_dir, directory); 261 auto abs_file = CompileCommand.AbsoluteFileName(abs_workdir, file); 262 auto abs_output = CompileCommand.AbsoluteFileName(abs_workdir, output); 263 // dfmt off 264 rval = CompileCommand( 265 CompileCommand.FileName(file), 266 abs_file, 267 abs_workdir, 268 CompileCommand.Command(command), 269 CompileCommand.Arguments(arguments), 270 CompileCommand.Output(output), 271 abs_output); 272 // dfmt on 273 } catch (Exception ex) { 274 logger.error("Unable to parse json: " ~ ex.msg).collectException; 275 } 276 277 return rval; 278 } 279 280 /** Parse a CompilationDatabase. 281 * 282 * Params: 283 * raw_input = the content of the CompilationDatabase. 284 * in_file = path to the compilation database file. 285 * out_range = range to write the output to. 286 */ 287 private void parseCommands(T)(string raw_input, CompileDbFile in_file, ref T out_range) nothrow { 288 import std.json : parseJSON, JSONException; 289 290 static void put(T)(JSONValue v, AbsoluteCompileDbDirectory dbdir, ref T out_range) nothrow { 291 import std.algorithm : map, filter; 292 import std.array : array; 293 294 try { 295 // dfmt off 296 foreach (e; v.array() 297 // map the JSON tuples to D structs 298 .map!(a => toCompileCommand(a, dbdir)) 299 // remove invalid 300 .filter!(a => !a.isNull) 301 .map!(a => a.get)) { 302 out_range.put(e); 303 } 304 // dfmt on 305 } catch (Exception ex) { 306 logger.error("Unable to parse json:" ~ ex.msg).collectException; 307 } 308 } 309 310 try { 311 // trusted: is@safe in DMD-2.077.0 312 // remove the trusted attribute when the minimal requirement is upgraded. 313 auto json = () @trusted{ return parseJSON(raw_input); }(); 314 auto as_dir = AbsoluteCompileDbDirectory(in_file); 315 316 // trusted: this function is private so the only user of it is this module. 317 // the only problem would be in the out_range. It is assumed that the 318 // out_range takes care of the validation and other security aspects. 319 () @trusted{ put(json, as_dir, out_range); }(); 320 } catch (Exception ex) { 321 logger.error("Error while parsing compilation database: " ~ ex.msg).collectException; 322 } 323 } 324 325 void fromFile(T)(CompileDbFile filename, ref T app) { 326 import std.algorithm : joiner; 327 import std.conv : text; 328 import std.stdio : File; 329 330 // trusted: using the GC for memory management. 331 // assuming any UTF-8 errors in the input is validated by phobos byLineCopy. 332 auto raw = () @trusted{ 333 return File(cast(string) filename).byLineCopy.joiner.text; 334 }(); 335 336 raw.parseCommands(filename, app); 337 } 338 339 void fromFiles(T)(CompileDbFile[] fnames, ref T app) { 340 import std.file : exists; 341 342 foreach (f; fnames) { 343 if (!exists(f)) 344 throw new Exception("File do not exist: " ~ f); 345 f.fromFile(app); 346 } 347 } 348 349 /** Return default path if argument is null. 350 */ 351 CompileDbFile[] orDefaultDb(string[] cli_path) @safe pure nothrow { 352 import std.array : array; 353 import std.algorithm : map; 354 355 if (cli_path.length == 0) { 356 return [CompileDbFile("compile_commands.json")]; 357 } 358 359 return cli_path.map!(a => CompileDbFile(a)).array(); 360 } 361 362 /** Contains the results of a search in the compilation database. 363 * 364 * When searching for the compile command for a file, the compilation db can 365 * return several commands, as the file may have been compiled with different 366 * options in different parts of the project. 367 * 368 * Params: 369 * glob = glob pattern to find a matching file in the DB against 370 */ 371 CompileCommandSearch find(CompileCommandDB db, string glob) @safe 372 in { 373 debug logger.trace("Looking for " ~ glob); 374 } 375 out (result) { 376 import std.conv : to; 377 378 debug logger.trace("Found " ~ to!string(result)); 379 } 380 body { 381 import std.path : globMatch; 382 383 foreach (a; db) { 384 if (a.absoluteFile == glob) 385 return CompileCommandSearch([a]); 386 else if (a.file == glob) 387 return CompileCommandSearch([a]); 388 else if (globMatch(a.absoluteFile, glob)) 389 return CompileCommandSearch([a]); 390 else if (a.absoluteOutput == glob) 391 return CompileCommandSearch([a]); 392 else if (a.output == glob) 393 return CompileCommandSearch([a]); 394 else if (globMatch(a.absoluteOutput, glob)) 395 return CompileCommandSearch([a]); 396 } 397 398 logger.errorf("\n%s\nNo match found in the compile command database", db.toString); 399 400 return CompileCommandSearch(); 401 } 402 403 struct SearchResult { 404 string[] cflags; 405 AbsolutePath absoluteFile; 406 } 407 408 /** Append the compiler flags if a match is found in the DB or error out. 409 */ 410 Nullable!(SearchResult) appendOrError(CompileCommandDB compile_db, 411 const string[] cflags, const string input_file) @safe { 412 413 return appendOrError(compile_db, cflags, input_file, defaultCompilerFilter); 414 } 415 416 /** Append the compiler flags if a match is found in the DB or error out. 417 */ 418 Nullable!(SearchResult) appendOrError(CompileCommandDB compile_db, 419 const string[] cflags, const string input_file, const CompileCommandFilter flag_filter) @safe { 420 auto compile_commands = compile_db.find(input_file.idup); 421 debug { 422 logger.trace(compile_commands.length > 0, 423 "CompilationDatabase match (by filename):\n", compile_commands.toString); 424 if (compile_commands.length == 0) { 425 logger.trace(compile_db.toString); 426 } 427 428 logger.tracef("CompilationDatabase filter: %s", flag_filter); 429 } 430 431 typeof(return) rval; 432 if (compile_commands.length == 0) { 433 logger.warning("File not found in compilation database: ", input_file); 434 return rval; 435 } else { 436 rval = SearchResult.init; 437 rval.cflags = cflags ~ compile_commands[0].parseFlag(flag_filter); 438 rval.absoluteFile = compile_commands[0].absoluteFile; 439 } 440 441 return rval; 442 } 443 444 string toString(CompileCommand[] db) @safe pure { 445 import std.array; 446 import std.algorithm : map, joiner; 447 import std.conv : text; 448 import std.format : formattedWrite; 449 450 auto app = appender!string(); 451 452 foreach (a; db) { 453 formattedWrite(app, "%s\n %s\n %s\n", a.directory, a.file, a.absoluteFile); 454 455 if (a.output.hasValue) { 456 formattedWrite(app, " %s\n", a.output); 457 formattedWrite(app, " %s\n", a.absoluteOutput); 458 } 459 460 if (a.command.hasValue) 461 formattedWrite(app, " %-(%s %)\n", a.command); 462 463 if (a.arguments.hasValue) 464 formattedWrite(app, " %-(%s %)\n", a.arguments); 465 } 466 467 return app.data; 468 } 469 470 string toString(CompileCommandDB db) @safe pure { 471 return toString(db.payload); 472 } 473 474 string toString(CompileCommandSearch search) @safe pure { 475 return toString(search.payload); 476 } 477 478 const auto defaultCompilerFilter = CompileCommandFilter(defaultCompilerFlagFilter, 1); 479 480 /// Returns: array of default flags to exclude. 481 auto defaultCompilerFlagFilter() @safe { 482 import std.array : appender; 483 484 auto app = appender!(FilterClangFlag[])(); 485 486 // dfmt off 487 foreach (f; [ 488 // remove basic compile flag irrelevant for AST generation 489 "-c", "-o", 490 // machine dependent flags 491 "-m", 492 // machine dependent flags, AVR 493 "-nodevicelib", "-Waddr-space-convert", 494 // machine dependent flags, VxWorks 495 "-non-static", "-Bstatic", "-Bdynamic", "-Xbind-lazy", "-Xbind-now", 496 // blacklist all -f because most aren not compatible with clang 497 "-f", 498 // linker flags, irrelevant for the AST 499 "-static", "-shared", "-rdynamic", "-s", "-l", "-L", "-z", "-u", "-T", "-Xlinker", 500 // a linker flag with filename as one argument 501 "-l", 502 // remove some of the preprocessor flags, irrelevant for the AST 503 "-MT", "-MF", "-MD", "-MQ", "-MMD", "-MP", "-MG", "-E", "-cc1", "-S", "-M", "-MM", "-###", 504 ]) { 505 app.put(FilterClangFlag(f)); 506 } 507 // dfmt on 508 509 return app.data; 510 } 511 512 struct CompileCommandFilter { 513 FilterClangFlag[] filter; 514 int skipCompilerArgs = 1; 515 } 516 517 /// Parsed compiler flags. 518 struct ParseFlags { 519 /// The includes used in the compile command 520 static struct Includes { 521 string[] payload; 522 alias payload this; 523 } 524 525 /// 526 Includes includes; 527 528 string[] flags; 529 alias flags this; 530 } 531 532 /** Filter and normalize the compiler flags. 533 * 534 * - Sanitize the compiler command by removing flags matching the filter. 535 * - Remove excess white space. 536 * - Convert all filenames to absolute path. 537 */ 538 ParseFlags parseFlag(const CompileCommand cmd, const CompileCommandFilter flag_filter) @safe { 539 import std.algorithm : among; 540 541 static bool excludeStartWith(const string raw_flag, const FilterClangFlag[] flag_filter) @safe { 542 import std.algorithm : startsWith, filter, count; 543 import std.array : split, empty; 544 545 // the purpuse is to find if any of the flags in flag_filter matches 546 // the start of flag. 547 548 bool delegate(const FilterClangFlag) @safe cmp; 549 550 const parts = raw_flag.split('='); 551 if (parts.length == 2) { 552 // is a -foo=bar flag thus exact match is the only sensible 553 cmp = (const FilterClangFlag a) => parts[0] == a.payload; 554 } else { 555 // the flag has the argument merged thus have to check if the start match 556 cmp = (const FilterClangFlag a) => raw_flag.startsWith(a.payload); 557 } 558 559 // dfmt off 560 return 0 != flag_filter 561 .filter!(a => a.kind == FilterClangFlag.Kind.exclude) 562 // keep flags that are at least the length of values 563 .filter!(a => raw_flag.length >= a.length) 564 // if the flag is any of those in filter 565 .filter!cmp 566 .count(); 567 // dfmt on 568 } 569 570 static bool isCombinedIncludeFlag(string flag) @safe { 571 // if an include flag make it absolute, as one argument by checking 572 // length. 3 is to only match those that are -Ixyz 573 return flag.length >= 3 && flag[0 .. 2] == "-I"; 574 } 575 576 static bool isNotAFlag(string flag) @safe { 577 // good enough if it seem to be a file 578 return flag.length >= 1 && flag[0] != '-'; 579 } 580 581 /// Flags that take an argument that is a path that need to be transformed 582 /// to an absolute path. 583 static bool isFlagAndPath(string flag) @safe { 584 // list derived from clang --help 585 return 0 != flag.among("-I", "-idirafter", "-iframework", "-imacros", 586 "-include-pch", "-include", "-iquote", "-isysroot", "-isystem-after", "-isystem"); 587 } 588 589 /// Flags that take an argument that is NOT a path. 590 static bool isFlagAndValue(string flag) @safe { 591 return 0 != flag.among("-D"); 592 } 593 594 static ParseFlags filterPair(T)(ref T r, CompileCommand.AbsoluteDirectory workdir, 595 const FilterClangFlag[] flag_filter, bool keepFirstArg) @safe { 596 enum State { 597 /// first argument is kept even though it isn't a flag because it is the command 598 firstArg, 599 /// keep the next flag IF none of the other transitions happens 600 keep, 601 /// forcefully keep the next argument as raw data 602 priorityKeepNextArg, 603 /// keep the next argument and transform to an absolute path 604 pathArgumentToAbsolute, 605 /// skip the next arg 606 skip, 607 /// skip the next arg, if it is not a flag 608 skipIfNotFlag, 609 } 610 611 import std.path : buildNormalizedPath, absolutePath; 612 import std.array : appender; 613 import std.range : ElementType; 614 615 auto st = keepFirstArg ? State.firstArg : State.keep; 616 auto rval = appender!(string[]); 617 auto includes = appender!(string[]); 618 619 foreach (arg; r) { 620 // First states and how to handle those. 621 // Then transitions from the state keep, which is the default state. 622 // 623 // The user controlled excludeStartWith must be before any other 624 // conditions after the states. It is to give the user the ability 625 // to filter out any flag. 626 627 if (st == State.firstArg) { 628 // keep it, it is the command 629 rval.put(arg); 630 st = State.keep; 631 } else if (st == State.skip) { 632 st = State.keep; 633 } else if (st == State.skipIfNotFlag && isNotAFlag(arg)) { 634 st = State.keep; 635 } else if (st == State.pathArgumentToAbsolute) { 636 st = State.keep; 637 auto p = buildNormalizedPath(workdir, arg).absolutePath; 638 rval.put(p); 639 includes.put(p); 640 } else if (st == State.priorityKeepNextArg) { 641 st = State.keep; 642 rval.put(arg); 643 } else if (excludeStartWith(arg, flag_filter)) { 644 st = State.skipIfNotFlag; 645 } else if (isCombinedIncludeFlag(arg)) { 646 rval.put("-I"); 647 auto p = buildNormalizedPath(workdir, arg[2 .. $]).absolutePath; 648 rval.put(p); 649 includes.put(p); 650 } else if (isFlagAndPath(arg)) { 651 rval.put(arg); 652 st = State.pathArgumentToAbsolute; 653 } else if (isFlagAndValue(arg)) { 654 rval.put(arg); 655 st = State.priorityKeepNextArg; 656 } // parameter that seem to be filenames, remove 657 else if (isNotAFlag(arg)) { 658 // skipping 659 } else { 660 rval.put(arg); 661 } 662 } 663 664 return ParseFlags(ParseFlags.Includes(includes.data), rval.data); 665 } 666 667 import std.algorithm : filter, splitter, min; 668 669 string[] pass1 = () @safe{ 670 // If `arguments` is used then it is already _perfect_. 671 if (cmd.arguments.hasValue) 672 return cmd.arguments.payload; 673 if (flag_filter.skipCompilerArgs != 0) 674 return cmd.command.payload; 675 // skip parameters matching the filter IF `command` where used. 676 return cmd.command[min(flag_filter.skipCompilerArgs, cmd.command.length) .. $]; 677 }().dup; 678 679 // `arguments` in a compilation database do not have the compiler binary in 680 // the string thus skipCompilerArgs isn't needed. 681 // This is different from the case where skipCompilerArgs is zero, which is 682 // intended to force filterPair that the first value in the range is the 683 // compiler, not a filename, and shall be kept. 684 bool keep_first_arg = !cmd.arguments.hasValue && flag_filter.skipCompilerArgs == 0; 685 686 return filterPair(pass1, cmd.directory, flag_filter.filter, keep_first_arg); 687 } 688 689 /// Import and merge many compilation databases into one DB. 690 CompileCommandDB fromArgCompileDb(string[] paths) @safe { 691 import std.array : appender; 692 693 auto app = appender!(CompileCommand[])(); 694 paths.orDefaultDb.fromFiles(app); 695 696 return CompileCommandDB(app.data); 697 } 698 699 /// Flags to exclude from the flags passed on to the clang parser. 700 struct FilterClangFlag { 701 string payload; 702 alias payload this; 703 704 enum Kind { 705 exclude 706 } 707 708 Kind kind; 709 } 710 711 @("Should be cflags with all unnecessary flags removed") 712 unittest { 713 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", 714 "bar", "-Igun", "-c", "a_filename.c"], AbsoluteCompileDbDirectory("/home"), null, null); 715 auto s = cmd.parseFlag(defaultCompilerFilter); 716 s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 717 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 718 } 719 720 @("Should be cflags with some excess spacing") 721 unittest { 722 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-MD", "-lfoo.a", "-l", 723 "bar.a", "-I", "bar", "-Igun"], AbsoluteCompileDbDirectory("/home"), null, null); 724 725 auto s = cmd.parseFlag(defaultCompilerFilter); 726 s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 727 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 728 } 729 730 @("Should be cflags with machine dependent removed") 731 unittest { 732 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-mfoo", "-m", "bar", 733 "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun", "-c", "a_filename.c"], 734 AbsoluteCompileDbDirectory("/home"), null, null); 735 736 auto s = cmd.parseFlag(defaultCompilerFilter); 737 s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 738 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 739 } 740 741 @("Should be cflags with all -f removed") 742 unittest { 743 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-fmany-fooo", "-I", "bar", "-fno-fooo", "-Igun", 744 "-flolol", "-c", "a_filename.c"], AbsoluteCompileDbDirectory("/home"), null, null); 745 746 auto s = cmd.parseFlag(defaultCompilerFilter); 747 s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 748 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 749 } 750 751 @("shall NOT remove -std=xyz flags") 752 unittest { 753 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-std=c++11", 754 "-c", "a_filename.c"], AbsoluteCompileDbDirectory("/home"), null, null); 755 756 auto s = cmd.parseFlag(defaultCompilerFilter); 757 s.shouldEqual(["-std=c++11"]); 758 } 759 760 @("Shall keep all compiler flags as they are") 761 unittest { 762 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-Da", "-D", 763 "b"], AbsoluteCompileDbDirectory("/home"), null, null); 764 765 auto s = cmd.parseFlag(defaultCompilerFilter); 766 s.shouldEqual(["-Da", "-D", "b"]); 767 } 768 769 version (unittest) { 770 import std.file : getcwd; 771 import std.path : absolutePath; 772 import std.format : format; 773 774 // contains a bit of extra junk that is expected to be removed 775 immutable string dummy_path = "/path/to/../to/./db/compilation_db.json"; 776 immutable string dummy_dir = "/path/to/db"; 777 778 enum raw_dummy1 = `[ 779 { 780 "directory": "dir1/dir2", 781 "command": "g++ -Idir1 -c -o binary file1.cpp", 782 "file": "file1.cpp" 783 } 784 ]`; 785 786 enum raw_dummy2 = `[ 787 { 788 "directory": "dir", 789 "command": "g++ -Idir1 -c -o binary file1.cpp", 790 "file": "file1.cpp" 791 }, 792 { 793 "directory": "dir", 794 "command": "g++ -Idir1 -c -o binary file2.cpp", 795 "file": "file2.cpp" 796 } 797 ]`; 798 799 enum raw_dummy3 = `[ 800 { 801 "directory": "dir1", 802 "command": "g++ -Idir1 -c -o binary file3.cpp", 803 "file": "file3.cpp" 804 }, 805 { 806 "directory": "dir2", 807 "command": "g++ -Idir1 -c -o binary file3.cpp", 808 "file": "file3.cpp" 809 } 810 ]`; 811 812 enum raw_dummy4 = `[ 813 { 814 "directory": "dir1", 815 "arguments": "-Idir1 -c -o binary file3.cpp", 816 "file": "file3.cpp", 817 "output": "file3.o" 818 }, 819 { 820 "directory": "dir2", 821 "arguments": "-Idir1 -c -o binary file3.cpp", 822 "file": "file3.cpp", 823 "output": "file3.o" 824 } 825 ]`; 826 827 enum raw_dummy5 = `[ 828 { 829 "directory": "dir1", 830 "arguments": ["-Idir1", "-c", "-o", "binary", "file3.cpp"], 831 "file": "file3.cpp", 832 "output": "file3.o" 833 }, 834 { 835 "directory": "dir2", 836 "arguments": ["-Idir1", "-c", "-o", "binary", "file3.cpp"], 837 "file": "file3.cpp", 838 "output": "file3.o" 839 } 840 ]`; 841 } 842 843 version (unittest) { 844 import std.array : appender; 845 import unit_threaded : writelnUt; 846 } 847 848 @("Should be a compile command DB") 849 unittest { 850 auto app = appender!(CompileCommand[])(); 851 raw_dummy1.parseCommands(CompileDbFile(dummy_path), app); 852 auto cmds = app.data; 853 854 assert(cmds.length == 1); 855 cmds[0].directory.shouldEqual(dummy_dir ~ "/dir1/dir2"); 856 cmds[0].command.shouldEqual(["g++", "-Idir1", "-c", "-o", "binary", "file1.cpp"]); 857 cmds[0].file.shouldEqual("file1.cpp"); 858 cmds[0].absoluteFile.shouldEqual(dummy_dir ~ "/dir1/dir2/file1.cpp"); 859 } 860 861 @("Should be a DB with two entries") 862 unittest { 863 auto app = appender!(CompileCommand[])(); 864 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 865 auto cmds = app.data; 866 867 cmds[0].file.shouldEqual("file1.cpp"); 868 cmds[1].file.shouldEqual("file2.cpp"); 869 } 870 871 @("Should find filename") 872 unittest { 873 auto app = appender!(CompileCommand[])(); 874 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 875 auto cmds = CompileCommandDB(app.data); 876 877 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 878 assert(found.length == 1); 879 found[0].file.shouldEqual("file2.cpp"); 880 } 881 882 @("Should find no match by using an absolute path that doesn't exist in DB") 883 unittest { 884 auto app = appender!(CompileCommand[])(); 885 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 886 auto cmds = CompileCommandDB(app.data); 887 888 auto found = cmds.find("./file2.cpp"); 889 assert(found.length == 0); 890 } 891 892 @("Should find one match by using the absolute filename to disambiguous") 893 unittest { 894 import unit_threaded : writelnUt; 895 896 auto app = appender!(CompileCommand[])(); 897 raw_dummy3.parseCommands(CompileDbFile(dummy_path), app); 898 auto cmds = CompileCommandDB(app.data); 899 900 auto found = cmds.find(dummy_dir ~ "/dir2/file3.cpp"); 901 assert(found.length == 1); 902 903 found.toString.shouldEqual(format("%s/dir2 904 file3.cpp 905 %s/dir2/file3.cpp 906 g++ -Idir1 -c -o binary file3.cpp 907 ", dummy_dir, dummy_dir)); 908 } 909 910 @("Should be a pretty printed search result") 911 unittest { 912 auto app = appender!(CompileCommand[])(); 913 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 914 auto cmds = CompileCommandDB(app.data); 915 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 916 917 found.toString.shouldEqual(format("%s/dir 918 file2.cpp 919 %s/dir/file2.cpp 920 g++ -Idir1 -c -o binary file2.cpp 921 ", dummy_dir, dummy_dir)); 922 } 923 924 @("Should be a compile command DB with relative path") 925 unittest { 926 enum raw = `[ 927 { 928 "directory": ".", 929 "command": "g++ -Idir1 -c -o binary file1.cpp", 930 "file": "file1.cpp" 931 } 932 ]`; 933 auto app = appender!(CompileCommand[])(); 934 raw.parseCommands(CompileDbFile(dummy_path), app); 935 auto cmds = app.data; 936 937 assert(cmds.length == 1); 938 cmds[0].directory.shouldEqual(dummy_dir); 939 cmds[0].file.shouldEqual("file1.cpp"); 940 cmds[0].absoluteFile.shouldEqual(dummy_dir ~ "/file1.cpp"); 941 } 942 943 @("Should be a DB read from a relative path with the contained paths adjusted appropriately") 944 unittest { 945 auto app = appender!(CompileCommand[])(); 946 raw_dummy3.parseCommands(CompileDbFile("path/compile_db.json"), app); 947 auto cmds = CompileCommandDB(app.data); 948 949 // trusted: constructing a path in memory which is never used for writing. 950 auto abs_path = () @trusted{ return getcwd() ~ "/path"; }(); 951 952 auto found = cmds.find(abs_path ~ "/dir2/file3.cpp"); 953 assert(found.length == 1); 954 955 found.toString.shouldEqual(format("%s/dir2 956 file3.cpp 957 %s/dir2/file3.cpp 958 g++ -Idir1 -c -o binary file3.cpp 959 ", abs_path, abs_path)); 960 } 961 962 @("shall extract arguments, file, directory and output with absolute paths") 963 unittest { 964 auto app = appender!(CompileCommand[])(); 965 raw_dummy4.parseCommands(CompileDbFile("path/compile_db.json"), app); 966 auto cmds = CompileCommandDB(app.data); 967 968 // trusted: constructing a path in memory which is never used for writing. 969 auto abs_path = () @trusted{ return getcwd() ~ "/path"; }(); 970 971 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 972 assert(found.length == 1); 973 974 found.toString.shouldEqual(format("%s/dir2 975 file3.cpp 976 %s/dir2/file3.cpp 977 file3.o 978 %s/dir2/file3.o 979 -Idir1 -c -o binary file3.cpp 980 ", abs_path, abs_path, abs_path)); 981 } 982 983 @("shall be the compiler flags derived from the arguments attribute") 984 unittest { 985 auto app = appender!(CompileCommand[])(); 986 raw_dummy4.parseCommands(CompileDbFile("path/compile_db.json"), app); 987 auto cmds = CompileCommandDB(app.data); 988 989 // trusted: constructing a path in memory which is never used for writing. 990 auto abs_path = () @trusted{ return getcwd() ~ "/path"; }(); 991 992 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 993 assert(found.length == 1); 994 995 found[0].parseFlag(defaultCompilerFilter).flags.shouldEqual(["-I", 996 buildPath(abs_path, "dir2", "dir1")]); 997 } 998 999 @("shall find the entry based on an output match") 1000 unittest { 1001 auto app = appender!(CompileCommand[])(); 1002 raw_dummy4.parseCommands(CompileDbFile("path/compile_db.json"), app); 1003 auto cmds = CompileCommandDB(app.data); 1004 1005 // trusted: constructing a path in memory which is never used for writing. 1006 auto abs_path = () @trusted{ return getcwd() ~ "/path"; }(); 1007 1008 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1009 assert(found.length == 1); 1010 1011 found[0].absoluteFile.shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1012 } 1013 1014 @("shall parse the compilation database when *arguments* is a json list") 1015 unittest { 1016 auto app = appender!(CompileCommand[])(); 1017 raw_dummy5.parseCommands(CompileDbFile("path/compile_db.json"), app); 1018 auto cmds = CompileCommandDB(app.data); 1019 1020 // trusted: constructing a path in memory which is never used for writing. 1021 auto abs_path = () @trusted{ return getcwd() ~ "/path"; }(); 1022 1023 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1024 assert(found.length == 1); 1025 1026 found[0].absoluteFile.shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1027 } 1028 1029 @("shall parse the compilation database and find a match via the glob pattern") 1030 unittest { 1031 import std.path : baseName; 1032 1033 auto app = appender!(CompileCommand[])(); 1034 raw_dummy5.parseCommands(CompileDbFile("path/compile_db.json"), app); 1035 auto cmds = CompileCommandDB(app.data); 1036 1037 auto found = cmds.find("*/dir2/file3.cpp"); 1038 assert(found.length == 1); 1039 1040 found[0].absoluteFile.baseName.shouldEqual("file3.cpp"); 1041 }